package jxtend.processor.eclipse.visitor.handler.impl;

import java.lang.reflect.Constructor;
import java.util.Arrays;

import jxtend.processor.eclipse.utils.ASTNodeKind;
import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;
import jxtend.processor.eclipse.visitor.handler.ASTNodeHandlerVisitorContext;
import jxtend.processor.eclipse.visitor.handler.AbstractASTNodeVisitorHandler;
import jxtend.processor.eclipse.visitor.handler.ProblemSeverity;
import jxtend.processor.eclipse.visitor.handler.StatementTypeResolver;
import jxtend.processor.reflect.ReflectField;
import jxtend.processor.reflect.ReflectMethod;
import jxtend.processor.reflect.ReflectMethod.ResultMethod4Args;
import jxtend.processor.utils.ReflectUtils;

import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.ArrayInitializer;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.ForeachStatement;
import org.eclipse.jdt.internal.compiler.ast.LocalDeclaration;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BlockScope;
import org.eclipse.jdt.internal.compiler.lookup.InferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.MethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.MissingTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedGenericMethodBinding;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding;
import org.eclipse.jdt.internal.compiler.lookup.WildcardBinding;

@SuppressWarnings("restriction")
public class TypeInferenceHandler extends AbstractASTNodeVisitorHandler implements StatementTypeResolver {

	private static final ReflectField<InferenceContext, TypeBinding[]> substitutesOfInferenceContext =
		ReflectField.getField(InferenceContext.class, TypeBinding[].class, "substitutes");		

	private static final ResultMethod4Args<ParameterizedGenericMethodBinding, Boolean, Scope, TypeVariableBinding[],
			InferenceContext, Boolean> resolveSubstituteConstraintsMethod =
		ReflectMethod.method(ParameterizedGenericMethodBinding.class, Boolean.class,  "resolveSubstituteConstraints",
				Scope.class,  TypeVariableBinding[].class, InferenceContext.class, boolean.class);

	private static final  Constructor<MethodBinding> methodBindingConstructor =
			ReflectUtils.getInaccessibleConstructor(MethodBinding.class);

	@Override
	public boolean resolveStatement(ASTNodeHandlerVisitorContext context, Statement node, Scope scope) {
		ASTNode parentNode = context.getParentNode();
	
		if (!(node instanceof ArrayInitializer)
				|| (((ArrayInitializer) node).expressions == null)
				|| (((ArrayInitializer) node).expressions.length == 0)
				|| !(parentNode instanceof LocalDeclaration)
				|| !TypeUtils.isValOrVarType(((LocalDeclaration) parentNode).type)
				|| (((LocalDeclaration) parentNode).initialization == null)
				|| !(scope instanceof BlockScope)) {
			return false;
		}

		ArrayInitializer arrayInitializer = (ArrayInitializer) node;

		boolean eachExpressionHasSameType = eachExpressionHasSameType(arrayInitializer.expressions, (BlockScope) scope);

		if (eachExpressionHasSameType) {
			((ArrayInitializer) node).binding =
				new ArrayBinding(arrayInitializer.expressions[0].resolvedType, 1, context.getLookupEnvironment());

			return true;
		}

		MethodBinding methodBinding = ReflectUtils.newInstance(methodBindingConstructor);

		TypeVariableBinding typeVariableBinding =
			new TypeVariableBinding("T".toCharArray(), methodBinding, 0, context.getLookupEnvironment());

		methodBinding.typeVariables = new TypeVariableBinding[] { typeVariableBinding };

		InferenceContext inferenceContext = new InferenceContext(methodBinding);

		for (Expression expression : arrayInitializer.expressions) {
			context.resolveExpression(expression, (BlockScope) scope);

			typeVariableBinding.collectSubstitutes(
					scope, expression.resolvedType, inferenceContext, TypeConstants.CONSTRAINT_EXTENDS);
		}

		boolean succeeded = resolveSubstituteConstraintsMethod.invoke((ParameterizedGenericMethodBinding) null,
				scope, new TypeVariableBinding[] { typeVariableBinding }, inferenceContext, false);

		if (!succeeded) {
			return true;
		}

		TypeBinding[] typeBindings = substitutesOfInferenceContext.get(inferenceContext);
		assert typeBindings.length == 1;

		TypeBinding resultTypeBinding = typeBindings[0];

		if (resultTypeBinding instanceof WildcardBinding) {
			WildcardBinding wildcardBinding = (WildcardBinding) resultTypeBinding;

			resultTypeBinding = wildcardBinding.bound;
		}

		// no 'resolvedType' exists in 'ArrayInitializer' class, instead we set the binding to hold the resolved type
		((ArrayInitializer) node).binding = new ArrayBinding(resultTypeBinding, 1, context.getLookupEnvironment());

		// return true; we have already resolved the statement and don't want it to be resolved once again 
		return true;
	}

	private static boolean eachExpressionHasSameType(Expression[] expressions, BlockScope blockScope) {
		assert expressions.length != 0;

		TypeBinding previousResolvedType = null;
		for (Expression expression : expressions) {
			assert (expression != null); // && (expression.resolvedType != null);

			expression.resolve(blockScope);

			TypeBinding resolvedType = expression.resolvedType;
			assert resolvedType != null;

			if ((previousResolvedType != null) && !previousResolvedType.equals(expression.resolvedType)) {
				return false;
			}

			previousResolvedType = resolvedType;
		}

		return (previousResolvedType != null);
	}

	@Override
	public boolean shouldResolve(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		ASTNode parentNode = context.getParentNode();
		if ((parentNode instanceof LocalDeclaration && TypeUtils.isValOrVarType(((LocalDeclaration) parentNode).type)
					&& node == ((LocalDeclaration) parentNode).initialization)
				|| (parentNode instanceof ForeachStatement
						&& TypeUtils.isValOrVarType(((ForeachStatement) parentNode).elementVariable.type)
						&& node == ((ForeachStatement) parentNode).collection)) {
			return true;
		}

		return super.shouldResolve(context, node, scope);
	}
	
	@Override
	public boolean endVisitBeforeResolve(ASTNodeHandlerVisitorContext context, ASTNode node, Scope scope) {
		LocalDeclaration valOrVarDeclaration = getValOrVarDeclaration(context, node);

		if ((valOrVarDeclaration == null) || (node != valOrVarDeclaration.type)) {
			return true;
		}

		TypeBinding nodeTypeBinding = null;

		ASTNode parentOfParentNode = context.getParentNode(1);		
		
		if (parentOfParentNode instanceof ForeachStatement) {
			ForeachStatement foreachStatement = (ForeachStatement) parentOfParentNode;
			final TypeBinding resolvedCollectionType = foreachStatement.collection.resolvedType;

			if (resolvedCollectionType instanceof ArrayBinding) {
				final ArrayBinding arrayBinding = (ArrayBinding) resolvedCollectionType;

				if (arrayBinding.dimensions > 1) {
					nodeTypeBinding = new ArrayBinding(arrayBinding.leafComponentType,
							arrayBinding.dimensions - 1, context.getLookupEnvironment());
				} else {
					assert arrayBinding.dimensions == 1;

					nodeTypeBinding = arrayBinding.leafComponentType;
				}

			} else if ((resolvedCollectionType != null)
					&& EclipseProcessorUtils.isValidTypeReference(resolvedCollectionType)) {
				ReferenceBinding iterableBinding = scope.getJavaLangIterable();
				if (resolvedCollectionType instanceof ParameterizedTypeBinding) {
					iterableBinding = new ParameterizedTypeBinding(iterableBinding,
							((ParameterizedTypeBinding) resolvedCollectionType).arguments, null,
							context.getLookupEnvironment());
				}

				if (resolvedCollectionType.isCompatibleWith(iterableBinding)) {
					if (resolvedCollectionType instanceof ParameterizedTypeBinding) {
						final ParameterizedTypeBinding parameterizedTypeBinding =
							(ParameterizedTypeBinding) resolvedCollectionType;

						if ((parameterizedTypeBinding.arguments != null)
								&& (parameterizedTypeBinding.arguments.length == 1)) {
							if (parameterizedTypeBinding.arguments[0] instanceof WildcardBinding) {
								nodeTypeBinding = ((WildcardBinding) parameterizedTypeBinding.arguments[0]).bound;
							} else if (EclipseProcessorUtils.isValidTypeReference(
									parameterizedTypeBinding.arguments[0])) {
								nodeTypeBinding = (ReferenceBinding) parameterizedTypeBinding.arguments[0];
							}
						}
					} else {
						nodeTypeBinding = scope.getJavaLangIterable();
					}
				}
			}
		} else {
			Expression initialization = ((LocalDeclaration) context.getParentNode()).initialization;
			
			nodeTypeBinding = (initialization instanceof ArrayInitializer)
				? ((ArrayInitializer) initialization).binding : ((Expression) initialization).resolvedType;
		}

		if ((nodeTypeBinding == null) || (nodeTypeBinding instanceof MissingTypeBinding)) {
			return false;
		}

		SingleTypeReference valOrVarTypeReference = (SingleTypeReference) valOrVarDeclaration.type;	

		ASTNodeKind astNodeKind = ASTNodeKind.getEnum(valOrVarTypeReference);

		boolean isValType = Arrays.equals(valOrVarTypeReference.token, JxtendTypeConstants.VAL);
		if (isValType) {
			valOrVarDeclaration.modifiers |= ClassFileConstants.AccFinal;
		} else {
			if ((valOrVarDeclaration.modifiers & ClassFileConstants.AccFinal) != 0) {
				context.recordProblemForNode(ProblemSeverity.ERROR, context.getCurrentReferenceContext(),
						valOrVarDeclaration.type.sourceStart, valOrVarDeclaration.type.sourceEnd,
						"No explicit final allowed for a 'var'");
			}
		}

		context.replaceNode(ASTNodeUtils.createTypeReferenceForBinding(
				astNodeKind, nodeTypeBinding, valOrVarTypeReference.sourceStart, valOrVarTypeReference.sourceEnd));

		return true;
	}

	private static LocalDeclaration getValOrVarDeclaration(ASTNodeHandlerVisitorContext context, ASTNode node) {
		if (!(node instanceof TypeReference) || !TypeUtils.isValOrVarType((TypeReference) node)) {
			return null;
		}

		ASTNode parentNode = context.getParentNode();

		if (!(parentNode instanceof LocalDeclaration)) {
			return null;
		}
		
		ASTNode parentOfParentNode = context.getParentNode(1);
		
		if (((parentOfParentNode instanceof ForeachStatement)
					&& ((ForeachStatement) parentOfParentNode).collection == null)
				|| (!(parentOfParentNode instanceof ForeachStatement) && (parentNode instanceof LocalDeclaration)
						&& ((LocalDeclaration) parentNode).initialization == null)) {
			return null;
		}

		return (LocalDeclaration) parentNode;
	}

}
